/// @author vtloc
/// @date 2014Jan09
/// @author GradualStudent
/// @date 2015jan06
/// This module's purpose is to transform 
/// 
var _ = require('lodash'); // replacing underscore to get merge function
var fs = require('fs');
var _chartSpecs = require('./charts');


function OfficeChart(chartInfo) {

  if (chartInfo instanceof OfficeChart) {
    return chartInfo;
  }

  return {

    chartSpec: null, // Javascript object that represents the XML tree for the PowerPoint chart

    toJSON: function () {
      return this.chartSpec;
    },

    getClass: function() { return "OfficeChart"; },

    ///
    /// @brief Create XML representation of chart object
    /// @param[chartInfo] object
    ///  {
    ///			title: 'eSurvey chart',
    ///			data:  [ // array of series
    ///				{
    ///					name: 'Income',
    ///					labels: ['2005', '2006', '2007', '2008', '2009'],
    ///					values: [23.5, 26.2, 30.1, 29.5, 24.6],
    ///         color: 'ff0000'
    ///				}
    ///			],
    ///     overlap:  "0",
    ///     gapWidth: "150"
    ///
    /// 	}

    initialize: function (chartInfo) {

      if (chartInfo.getClass &&  chartInfo.getClass()== 'OfficeChart') {
        return chartInfo;
      }

      // overlap ["50"] is handled as an option within the chartbase
      // gapWidth ["150"] is handled as an option within the chartbase
      // valAxisCrossAtMaxCategory [true|false] is handled as an option within the chart base
      // catAxisReverseOrder [true|false] is handled as an option within the chart base

      this.chartSpec = OfficeChart.getChartBase(chartInfo);  // get foundation XML for the chart type

      // Below are methods for handling options with more complex XML to mix in
      this.setData(chartInfo['data']);
      this.setTitle(chartInfo.title || chartInfo.name);
      this.setValAxisTitle(chartInfo.valAxisTitle);
      this.setCatAxisTitle(chartInfo.catAxisTitle);
      this.setValAxisNumFmt(chartInfo.valAxisNumFmt);
      this.setValAxisScale(chartInfo.valAxisMinValue, chartInfo.valAxisMaxValue);
      this.setTextSize(chartInfo.fontSize);
      this.mergeChartXml(chartInfo.xml);
      this.setValAxisMajorGridlines(chartInfo.valAxisMajorGridlines);
      this.setValAxisMinorGridlines(chartInfo.valAxisMinorGridlines);

      return this;
    },

    setTextSize: function(textSize) {
      if (textSize !== undefined) {
         var textRef = this._text(textSize);
        _.merge(this.chartSpec['c:chartSpace'], textRef);
      }
    },

    setTitle: function (title) {
      if (title !== undefined) {
        var titleRef = this._title(chartInfo.title || chartInfo.name);
        _.merge(this.chartSpec['c:chartSpace']['c:chart'], titleRef)
      }
    },

    setValAxisTitle: function (title) {
      if (title) {
        var titleRef = this._title(title);
        _.merge(this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'], titleRef);
      }
    },

    setCatAxisTitle: function (title) {
      if (title) {
        var titleRef = this._title(title);
        _.merge(this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:catAx'], titleRef);
      }
    },

    setValAxisNumFmt: function (format) {
      if (format !== undefined) {
        var numFmtRef = this._numFmt(format);
        _.merge(this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'], numFmtRef)
      }
    },

    setValAxisScale: function (min, max) {
      if (min!==undefined || max!== undefined) {
        var scalingRef = this._scaling(min, max);
        _.merge(this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'], scalingRef);
      }
    },

    mergeChartXml: function (xml) {
      if (xml !== undefined) {
        _.merge(this.chartSpec['c:chartSpace'], xml);
      }
    },

    setValAxisMajorGridlines: function(boolean) {
      if (boolean) {
        this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx']['c:majorGridlines'] = {};
      }
    },
    setValAxisMinorGridlines: function(boolean) {
      if (boolean) {
        this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx']['c:minorGridlines'] = {};
      }
    },


    setData: function (data) {

      if (data) {
        this.data = data;

        // Mix in data series
        var seriesDataRef;
        if (this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:barChart']) {
          seriesDataRef = this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:barChart']['#list'];
        }
        else if (this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:pieChart']) {
          seriesDataRef = this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:pieChart']['#list'];
        }
        else throw new Error("Can't add data to chart that is not initialized or not a recognized type")

        // the barChart/pieChart objects has other attributes too, so we push the series data on, not replace it
        if (chartInfo['data']) {
          var seriesData = this.getSeriesRef(chartInfo['data']);
          for (var i = 0; i < seriesData.length; i++) {
            seriesDataRef.push(seriesData[i])
          }
        }
      }
      return this;
    },


///
/// @brief Transform an array of string into an office's compliance structure
///
/// @param[in] region String
///		The reference cell of the string, for example: $A$1
/// @param[in] stringArr
///		An array of string, for example: ['foo', 'bar']
///
    _strRef: function (region, stringArr) {
      var obj = {
        'c:strRef': {
          'c:f': region,
          'c:strCache': function () {
            var result = {};
            result['c:ptCount'] = { '@val': stringArr.length };
            result['#list'] = [];
            for (var i = 0; i < stringArr.length; i++) {
              result['#list'].push({'c:pt': { '@idx': i, 'c:v': stringArr[i] }});
            }
            return result;
          }()
        }
      };

      return obj;
    },

///
/// @brief Transform an array of numbers into an office's compliance structure
///
/// @param[in] region String
///		The reference cell of the string, for example: $A$1
/// @param[in] numArr
///		An array of numArr, for example: [4, 7, 8]
/// @param[in] formatCode
///		A string describe the number's format. Example: General
///
    _numRef: function (region, numArr, formatCode) {
      var obj = {
        'c:numRef': {
          'c:f': region,
          'c:numCache': {
            'c:formatCode': formatCode,
            'c:ptCount': {'@val': '' + numArr.length},
            '#list': function () {
              result = [];
              for (var i = 0; i < numArr.length; i++) {
                result.push({'c:pt': { '@idx': i, 'c:v': numArr[i].toString() }});
              }

              return result;
            }
          }
        }
      };

      return obj;
    },

    _numFmt: function (formatCode) {
      return {
        "c:numFmt": {
          "@formatCode": formatCode ? formatCode : "General",
          "@sourceLinked": formatCode ? "0" : "1"
        }
      }
    },

///
/// @brief Transform an array of string into an office's compliance structure
///
/// @param[in] colorArr
///     An array of colorArr, for example: ['ff0000', '00ff00', '0000ff']
///
    _colorRef: function (colorArr) {
      var arr = [];
      for (var i = 0; i < colorArr.length; i++) {
        arr.push({
          'c:dPt': {
            'c:idx': {'@val': i},
            'c:bubble3D': {'@val': 0},
            'c:spPr': {
              'a:solidFill': {
                'a:srgbClr': {'@val': colorArr[i].toString()}
              }
            }
          }
        });
      }

      return arr;
    },

///
/// @brief Transform an array of string into an office's compliance structure
///
/// @param[in] row int
///		Row index.
/// @param[in] col int
///		Col index.
/// @param[in] isRowAbsolute boolean
///		Will add $ into cell's address if this parameter is true.
/// @param[in] isColAbsolute boolean
///		Will add $ into cell's address if this parameter is true.
///
    _rowColToSheetAddress: function (row, col, isRowAbsolute, isColAbsolute) {
      var address = "";

      if (isColAbsolute)
        address += '$';

      // these lines of code will transform the number 1-26 into A->Z
      // used in excel's cell's coordination
      while (col > 0) {
        var num = col % 26;
        col = (col - num ) / 26;
        address += String.fromCharCode(65 + num - 1);
      }

      if (isRowAbsolute)
        address += '$';

      address += row;

      return address;
    },

    /// @brief returns XML snippet for a chart dataseries
    _ser: function (serie, i) {
      var rc2a = this._rowColToSheetAddress; // shortcut

      var serieData = {
        'c:ser': {
          'c:idx': {'@val': i},
          'c:order': {'@val': i},
          'c:tx': this._strRef('Sheet1!' + rc2a(1, 2 + i, true, true), [serie.name]), // serie's value
          'c:invertIfNegative': {'@val': '0'},
          'c:cat': this._strRef('Sheet1!' + rc2a(2, 1, true, true) + ':' + rc2a(2 + serie.labels.length - 1, 1, true, true), serie.labels),
          'c:val': this._numRef('Sheet1!' + rc2a(2, 2 + i, true, true) + ':' + rc2a(2 + serie.labels.length - 1, 2 + i, true, true), serie.values, "General")
        }
      };

      if (serie.color) {
        serieData['c:ser']['c:spPr'] = {
          'a:solidFill': {
            'a:srgbClr': {'@val': serie.color}
          }
        };
      }
      else if (serie.schemeColor) {
        serieData['c:ser']['c:spPr'] = {
          'a:solidFill': {
            'a:schemeClr': {'@val': serie.schemeColor}
          }
        };
      }

      if (serie.xml) {
        serieData['c:ser'] = _.merge(serieData['c:ser'], serie.xml)
      }


      // for pie charts
      if (serie.colors) {
        serieData['c:ser']['#list'] = this._colorRef(serie.colors);
      }

      return serieData;
    },


    /// @brief returns XML snippet for a chart dataseries
    getSeriesRef: function (data) {
      return data.map(this._ser, this);
    },


    /// @brief returns XML snippet for axis number format
    ///  e.g. "$0" for US currency, "0%" for percentages
    xmlValAxisFormat: function (formatCode) {
      return {
        "c:chartSpace": {
          "c:chart": {
            "c:plotArea": {
              "c:valAx": {
                "c:majorGridlines": {},
                "c:numFmt": {
                  "@formatCode": formatCode,
                  "@sourceLinked": "0"
                }
              }
            }
          }
        }
      }
    },

    /// @brief returns XML snippet for an axis scale
    /// currently just min/max are supported
    _scaling: function (minValue, maxValue) {
      //<c:scaling><c:orientation val="minMax"/><c:max val="24.0"/></c:scaling>
      var ref = {
        "c:scaling": {
          "c:orientation": {
            "@val": "minMax"
          }
        }
      };
      if (maxValue != undefined) {
        ref["c:scaling"]["c:max"] = {
          "@val": "" + (maxValue || "")
        };
      }
      if (minValue != undefined) {
        ref["c:scaling"]["c:max"] = {
          "@val": "" + (minValue || "")
        };
      }
    },

    _text: function(textSize) {
      return {
        "c:txPr": {
          "a:bodyPr": {},
          "a:listStyle": {},
          "a:p": {
            "a:pPr": {
              "a:defRPr": {
                "@sz": textSize
              }
            },
            "a:endParaRPr": {
              "@lang": "en-US"
            }
          }
        }
      };
    },

    /// @brief returns XML snippet for an axis title
    _title: function (title) {
      if (typeof title == 'object') return title; // assume it's an XML representations
      return {
        "c:title": {
          "c:tx": {
            "c:rich": {
              "a:bodyPr": {},
              "a:lstStyle": {},
              "a:p": {
                "a:pPr": {
                  "a:defRPr": {}
                },
                "a:r": {
                  "a:rPr": {
                    "@lang": "en-US",
                    "@dirty": "0",
                    "@smtClean": "0"
                  },
                  "a:t": title
                },
                "a:endParaRPr": {
                  "@lang": "en-US",
                  "@dirty": "0"
                }
              }
            }
          },
          "c:layout": {},
          "c:overlay": {
            "@val": "0"
          }
        }
      }
    }

  }.initialize(chartInfo);
}

OfficeChart.getChartBase = function (chartInfo) {

  var chartBase;

  if (typeof chartInfo == 'string') {
    chartBase = _chartSpecs[chartInfo]();
  }
  else if (typeof chartInfo.renderType == 'string') {
    chartBase = _chartSpecs[chartInfo.renderType](chartInfo);
  }
  else if (chartInfo.xml) {
    chartBase = chartInfo.xml
  }
  else {
    throw new Error("invalid chart type")
  }
  // return deep copy so can create multiple charts from same base within one PowerPoint document
  return JSON.parse(JSON.stringify(chartBase));
}

module.exports = OfficeChart;

/***********************************************************************************************************************
 // Column chart
 new OfficeChart({
  title: 'eSurvey chart',
  renderType: 'column',
  data: [ // array of series
    {
      name: 'Income',
      labels: ['2005', '2006', '2007', '2008', '2009'],
      values: [23.5, 26.2, 30.1, 29.5, 24.6],
      colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
    },
    {
      name: 'Expense',
      labels: ['2005', '2006', '2007', '2008', '2009'],
      values: [18.1, 22.8, 23.9, 25.1, 25],
      colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
    }
  ]
});


 // Pie chart
 new OfficeChart({
  title: 'eSurvey chart',
  renderType: 'pie',
  data: [ // array of series
    {
      name: 'Income',
      labels: ['2005', '2006', '2007', '2008', '2009'],
      values: [23.5, 26.2, 30.1, 29.5, 24.6],
      colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
    }
  ]
});


 // Clustered bar chat
 new OfficeChart({
  title: 'eSurvey chart',
  renderType: 'group-bar',
  data: [ // array of series
    {
      name: 'Income',
      labels: ['2005', '2006', '2007', '2008', '2009'],
      values: [23.5, 26.2, 30.1, 29.5, 24.6],
      colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
    }
  ]
});

 ***********************************************************************************************************************/